In today’s digital landscape, securing sensitive data is of utmost importance. While HTTPS is a cornerstone of secure communication over the internet, it alone might not fully safeguard sensitive information under certain circumstances. For example, passwords, credit card numbers, or personal identifiers could still be at risk due to potential vulnerabilities such as:
- Man-in-the-Middle (MITM) Attacks: Despite HTTPS, an attacker who successfully strips or intercepts SSL/TLS traffic may gain access to unencrypted data.
- Compliance Requirements: Many regulatory frameworks mandate additional encryption for specific types of data, even when HTTPS is used.
- Replay Attacks: Encrypting sensitive data at the application layer can mitigate the risk of intercepted data being reused.
To address these concerns, encrypting sensitive information before transmitting it to the server provides an extra layer of security. Asymmetric encryption algorithms like RSA
are particularly suited for this purpose due to their ability to securely exchange information without requiring a shared secret.
Why RSA?
RSA is an asymmetric encryption algorithm that uses a pair of keys: a public key for encryption and a private key for decryption. This makes it ideal for securing sensitive data in client-server communications:
- Public-Key Encryption: The front-end uses the back-end’s public key to encrypt data. Even if intercepted, the encrypted data is unreadable without the corresponding private key.
- Private-Key Decryption: The back-end securely decrypts the data using its private key, ensuring that only the intended recipient can access the sensitive information.
-
Ease of Implementation:
RSA
is widely supported across programming languages and platforms, making it straightforward to integrate into existing systems.
In this article, we will demonstrate how to use RSA
encryption to protect sensitive data during client-server communication.
To illustrate RSA encryption in practice, we will generate a pair of public and private keys on the back-end. The back-end will securely share the public key with the front-end, enabling the front-end to encrypt sensitive data. This encrypted data is then sent to the back-end, where it is decrypted using the private key. By the end of this guide, you will understand how to effectively use RSA to secure communication between your front-end and back-end, providing an additional layer of protection for sensitive information.
Let’s dive into the implementation!
Implementation
In this simplified implementation, we have a C#
back-end and JavaScript
or TypeScript
front-end, and we follow these steps:
- Request a Public Key: The front-end initiates the process by requesting a new public key from the back-end before sending sensitive data.
-
Generate Key Pair on Back-end: The back-end generates a new pair of private and public keys using the
RSA
algorithm and sends the public key to the front-end. - Encrypt Data on Front-end: The sensitive data is encrypted on the front-end using the received public key.
- Decrypt and Consume on Back-end: The back-end decrypts the data using the private key and processes it securely.
This step-by-step approach ensures that sensitive data remains protected throughout its lifecycle, providing an additional layer of security beyond what HTTPS offers.
In this example, we use BouncyCastle
on our C#
back-end and jsencrypt
on front-end that can be either JavaScript
or TypeScript
.
Let’s proceed by importing the necessary back-end dependencies:
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Security.Cryptography;
Next, we define a class dedicated to generating new key pairs:
public class RSAKeyPair
{
public string PrivateKey;
public string PublicKey;
private static string ExportAsymmetricKey(AsymmetricKeyParameter key)
{
using (StringWriter writer = new StringWriter())
{
PemWriter pemWriter = new PemWriter(writer);
pemWriter.WriteObject(key);
pemWriter.Writer.Flush();
return writer.ToString();
}
}
public static RSAKeyPair Generate()
{
using (var rsa = new RSACryptoServiceProvider(2048))
{
RSAParameters parameters = rsa.ExportParameters(true);
AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetRsaKeyPair(parameters);
// Export private key in the default format (CSP format)
byte[] privateKeyCsp = rsa.ExportCspBlob(includePrivateParameters: true);
string privateKey = Convert.ToBase64String(privateKeyCsp);
// Export private and public keys in PKCS#8 PEM format
string publicKey = ExportAsymmetricKey(keyPair.Public);
return new RSAKeyPair()
{
PrivateKey = privateKey,
PublicKey = publicKey
};
}
}
}
Additionally, we define a class responsible for decrypting data using the private key:
public class RSADecryption
{
public static string Decrypt(string encryptedText, string privateKey)
{
using (var rsa = new RSACryptoServiceProvider(2048))
{
// Import the private key from CSP format (Base64-encoded private key)
byte[] privateKeyCsp = Convert.FromBase64String(privateKey);
rsa.ImportCspBlob(privateKeyCsp);
// Decode the Base64-encoded encrypted text
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
// Decrypt the data using OAEP padding
byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, false); // 'false' to turn off OAEP padding, because client-side doesn't support it
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}
When the front-end requests a new public key:
- The back-end generates the key pair using the classes defined earlier.
-
It then stores the private key along with a randomly generated key in a cache, set to expire after a short duration, such as one or two minutes (
cache[random key] = private key with expiration
). - The back-end then sends the public key and random key to the front-end.
- The front-end encrypts the data using the public key and sends it back to the back-end along with the random key. The random key ensures that multiple encryption-decryption processes can occur simultaneously without conflict.
- Finally, the back-end retrieves the private key using the random key provided by the front-end and decrypts the data. Afterward, the cached private key is disposed of.
The simplest step is encrypting the data on the front-end using jsencrypt
. Below is the TypeScript
function:
const encrypt = (text: string, publicKey: string) {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
const encryptedText = encryptor.encrypt(text);
return encryptedText;
}
That’s it! You’re all set to implement secure encryption and decryption in your system.